#! /usr/bin/env python

#make executable with: chmod +x pyBass.py

from Tkinter import *
import tkFont
import time
import threading
import math
import bisect
import midiBuffer
import mingus.core.chords as chords
from mingus.containers.Note import Note
from mingus.containers.NoteContainer import NoteContainer
import random

def cumsum(x):
        return reduce(lambda a,b: a+[a[len(a)-1]+b],x[1:],[x[0]])

class Player():
        def __init__(self,midiBuff,chan,prog,changes,beatsPerMeasure,subDivs,lo=0,hi=midiBuffer.MIDI_MAX):
                self.midiBuffer=midiBuff
                self.midiBuffer.programChange(prog,chan)
                self.chan=chan
                self.changes=changes
                self.subDivs=subDivs
                self.beatsPerMeasure=beatsPerMeasure
                self.lastNotes=set([])
                self.notes=set([])
                self.lo=lo
                self.hi=hi
                
        def doBeat(self,beat,subDiv,dur):
                self.chooseNotes(beat,subDiv)

                self.notes=set(self.notes)
                self.midiBuffer.notesOff(self.lastNotes-self.notes,self.chan)
                self.midiBuffer.notesOn(self.notes-self.lastNotes,self.vel,self.chan)
                self.lastNotes=self.notes

        def reset(self):
                self.midiBuffer.notesOff(self.lastNotes,self.chan)
                self.lastNotes=set([])
                self.notes=set([])

        def newChord(self,beat):
                prev=beat-1
                if prev<0:
                        prev=len(self.changes)-1
                return self.changes[beat]!=self.changes[prev]

        def root(self,beat):
                return self.chordTones(beat)[0]

        def chordTones(self,beat):
                return map(int,NoteContainer(chords.from_shorthand(self.changes[beat])))

        def scaleTones(self,beat):
                tones = self.chordTones(beat)
                root=min(tones)
                tones = map(lambda x:(x-root)%12,tones)
                #0 1  2 3  4 5 6  7 8  9 10 11
                #r b2 2 b3 3 4 b5 5 b6 6 b7 7

                if not ((1 in tones) or (2 in tones)):
                        tones.append(2) #major 2nd
                if not ((5 in tones) or (6 in tones)):
                        tones.append(5) #perfect 4th
                if not ((8 in tones) or (9 in tones)):
                        tones.append(9) #major 6th
                if not ((10 in tones) or (11 in tones)):
                        tones.append(11) #major 7th
                tones.sort()
                tones=map(lambda x:x+root,tones)
                return tones

        def colorTones(self,tones): #tones is a list of note numbers assumed to start with the root -- filter out any perfect roots/fifths in any octave
                return filter(lambda x:x%12 not in [tones[0]%12,(tones[0]+7)%12],tones)

        def stableTones(self,tones): #tones is a list of note numbers assumed to start with the root -- filter out anything other than roots, thirds, and perfect fifths
                stables = [(tones[0]+x)%12 for x in[0,3,4,7]]
                return filter(lambda x:x%12 in stables,tones)

        def inRange(self,tones,randOctave=False):
                if randOctave:
                        span=math.ceil((self.hi-self.lo)/12.0)
                        tones=map(lambda x:x%12+12*random.randint(0,span),tones)
                def fix(x):
                        while x>self.hi:
                                x-=12
                        while x<self.lo:
                                x+=12
                        return x
                return map(lambda x:fix(x),tones)

class Bass(Player):
        def __init__(self,midiBuff,chan,prog,changes,beatsPerMeasure,subDivs,lo=0,hi=midiBuffer.MIDI_MAX):
                Player.__init__(self,midiBuff,chan,prog,changes,beatsPerMeasure,subDivs,lo,hi)
                self.reset()

        def reset(self):
                self.goingUp=random.random()>.5
                self.target=self.inRange([self.root(0)],True)[0]
                Player.reset(self)

        def newTarget(self,beat):
                if beat%self.beatsPerMeasure == self.beatsPerMeasure-1:
                        beat+=1
                else:
                        beat+=2
                        
                if beat>=len(self.changes):
                        beat=0

                if beat==0: #or self.newChord(beat)
                        acceptables=[self.root(beat)]
                else:
                        acceptables=self.chordTones(beat)
                        #acceptables=self.stableTones(self.chordTones(beat))
                        #targets=self.colorTones(self.chordTones(beat))

                if random.random()>.9:
                        self.goingUp = not self.goingUp

                if random.random()>.9:
                        curr=random.choice(range(self.lo,self.hi+1))
                        if random.random()>.25:
                                self.goingUp=curr<list(self.notes)[0]
                else:
                        curr=list(self.notes)[0]
        
                self.target=self.getClosest(curr, acceptables)



        def getClosest(self,curr,acceptables,target=[]):

                if curr<=self.lo: 
                        self.goingUp=True
                elif curr>=self.hi:
                        self.goingUp=False

                if self.goingUp:
                        if target !=[] and curr>=target:
                                return target
                        else:
                                curr+=1
                else:
                        if target != [] and curr<= target:
                                return target
                        else:
                                curr-=1

                matches=filter(lambda x:x%12==curr%12,acceptables)
                if len(matches)>0:
                        return curr
                else:
                        return self.getClosest(curr,acceptables,target)

        def getPassing(self,beat):
                ends=[self.target,list(self.notes)[0]]
                opts = range(*sorted(ends))
                opts=set(opts)-set(ends)

                if len(opts)>0:
                        return random.choice(list(opts)) #choice doesn't work on sets?  should respec to work on iterables, not just sequences.
                else:
                        return self.target
                
        def chooseNotes(self,beat,subDiv):
                vel=90
                sigma=5
                emph=.1
                if beat%2==1:
                        self.vel=random.gauss(vel,sigma)
                else:
                        self.vel=random.gauss(vel*(1-emph),sigma)

                if subDiv==2:
                        self.vel*=1+emph
                elif subDiv==1:
                        self.vel*=1-emph

                newNote=True
                if subDiv==0 and (beat%self.beatsPerMeasure)%2==0:
                        self.notes=[self.target]
                        self.newTarget(beat)
                elif subDiv==0 or (subDiv==2 and (self.didMiddle or random.random()>.9)) or (subDiv==1 and random.random()>.99):
                        last=list(self.notes)[0]
                        if subDiv==0:
                                self.notes=[self.getClosest(last,self.scaleTones(beat),self.target)]
                        else:
                                self.notes=[self.getPassing(beat)]
                        fix=0
                        while self.notes[0]==self.target or self.notes[0]==last:
                                self.notes[0]-=fix
                                if not self.goingUp:
                                        fix=1
                                else:
                                        fix=-1
                                if random.random()>.5:
                                        fix*=2
                                if random.random()>.9:
                                        fix*=-1
                                self.notes[0]+=fix

                        if subDiv==1:
                                self.didMiddle=True
                else:
                        newNote=False

                if subDiv!=1:
                        self.didMiddle=False

                if newNote and len(self.lastNotes)>0 and self.notes[0]==list(self.lastNotes)[0]: #sets don't support len?  it's supposed to
                        self.midiBuffer.notesOff(self.lastNotes,self.chan)
                        self.lastNotes=set([])

class Solo(Bass): #subclassing Bass just to get getClosest(), which depends on having self.goingUp
        def __init__(self,midiBuff,chan,prog,changes,beatsPerMeasure,subDivs,lo=0,hi=midiBuffer.MIDI_MAX):
                Player.__init__(self,midiBuff,chan,prog,changes,beatsPerMeasure,subDivs,lo,hi)
                self.state="resting"
                self.lastState="resting"
                self.curr=random.choice(range(lo,hi))
                self.goingUp=random.random()>.5
                self.ideaStart=self.curr
                self.lastJump=0

                self.ints=[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 4]#, 5, 6, 7, 8]
                self.ints+=map(lambda x:x*-1,self.ints)
                self.ints+=["r", "r", "r"]
                self.generateIntervalIdea()

        def generateIntervalIdea(self):
                self.intervalIdea=[]
                while random.random()<.85:
                        self.intervalIdea.append(random.choice(self.ints))
                        last=self.intervalIdea[len(self.intervalIdea)-1]
                        if last!="r" and abs(last)>1 and random.random()>.25:
                                self.intervalIdea.append(-1*abs(last)/last)
                self.intervalIdea=[0,0,0,0,1,1,1,0,0,0,1,1,1,0,0,0,-1,-1,0,0,3,-1]
                print self.intervalIdea
        
        def chooseNotes(self,beat,subDiv):
                vel=80
                sigma=22
                emph=.2              
                        
                if subDiv==2:
                        self.vel=random.gauss(vel*(1+emph),sigma)
                elif subDiv==1:
                        self.vel=random.gauss(vel*(1-emph), sigma)
                else:
                        self.vel=random.gauss(vel, sigma)

                if random.random()>.995:
                        self.generateIntervalIdea()  


                if random.random()>.9:
                        self.goingUp = not self.goingUp

                if random.random()>.95:
                        self.curr=random.choice(range(self.lo,self.hi+1))
                        if random.random()>.25 and self.lastNotes!=set([]):
                                self.goingUp=self.curr<list(self.lastNotes)[0]

                if self.curr<=self.lo: 
                        self.goingUp=True
                elif self.curr>=self.hi:
                        self.goingUp=False

                if subDiv!=1 or random.random()>.85:
                        if self.state=="intervalIdea":
                                if self.ideaLoc>len(self.intervalIdea)-1:
                                        self.finishPhrase(beat)
                                else:
                                        if self.ideaLoc==0 and random.random()>.05:
                                                self.curr=self.ideaStart
                                        if self.intervalIdea[self.ideaLoc]!="r":
                                                if self.goingUp:
                                                        adj=-1
                                                else:
                                                        adj=1
                                                self.notes=self.inRange([self.getClosest(self.curr+self.intervalIdea[self.ideaLoc]+adj,self.scaleTones(beat))])
                                                if self.ideaLoc==0:
                                                        self.ideaStart=self.notes[0]
                                        else:
                                                self.notes=[]
                                        self.ideaLoc+=1
                                        if random.random()>.98:
                                                self.ideaLoc+=random.gauss(0,len(self.intervalIdea)/2)
                                                self.ideaLoc=int(round(max(0,self.ideaLoc)))
                        elif self.state=="inPhrase":
                                if random.random()>.95:
                                        self.finishPhrase(beat)
                                elif random.random()>.9:
                                        self.notes=[] #rest
                                elif random.random()>.7:
                                        if abs(self.lastJump)>1 and random.random()>.3:
                                                jump=-1*self.lastJump/abs(self.lastJump)
                                        else:
                                                jump=random.choice(filter(lambda x: x!="r",self.ints))
                                        self.lastJump=jump
                                        self.notes=self.inRange([self.getClosest(self.curr+jump,self.scaleTones(beat))])
                                else:
                                        self.notes=self.inRange([self.getClosest(self.curr,self.scaleTones(beat))])
                        elif self.state=="finishPhrase":
                                if self.newChord(beat) or random.random()>.5:
                                        self.notes=[]
                                        if random.random()>.85:
                                               self.state="intervalIdea"
                                               self.ideaLoc=0
                                        elif random.random()>.95:
                                                self.state="inPhrase"
                                        else:
                                                self.notes=[]
                                                self.state="resting"
                        elif self.state=="resting":
                                self.notes=[]
                                if random.random()>.92:
                                        self.state="intervalIdea"
                                        self.ideaLoc=0
                                elif random.random()>.95:
                                        self.state="inPhrase"

                        if self.notes!=[]:
                                self.curr=list(self.notes)[0]
                        if self.state!=self.lastState:
                                print self.state
                        self.lastState=self.state

        def finishPhrase(self,beat):
                self.state="finishPhrase"
                self.notes=self.inRange([self.getClosest(self.curr,self.chordTones(beat))])
                self.vel*=1+abs(random.gauss(0,3))
                        


class Comp(Player):
        def __init__(self,midiBuff,chan,prog,changes,beatsPerMeasure,subDivs,lo=0,hi=midiBuffer.MIDI_MAX):
                Player.__init__(self,midiBuff,chan,prog,changes,beatsPerMeasure,subDivs,lo,hi)
                self.colorOnly=True
        
        def chooseNotes(self,beat,subDiv):
                vel=90
                sigma=15
                emph=.2

                if random.random()>.99:
                        self.colorOnly=not self.colorOnly
                
                if subDiv==2:
                        self.vel=random.gauss(vel*(1+emph),sigma)
                else:
                        self.vel=random.gauss(vel*(1-emph), sigma)
                        
                if subDiv==0 and self.newChord(beat):
                        self.notes=[]
                if subDiv in [0, 2]:
                        if random.random()>.5:
                                if random.random()>.65 and list(self.notes)==[]:
                                        self.lo+=random.gauss(0,3)
                                        gbBass=[50,80]
                                        norm=[45,65]
                                        inst=norm
                                        while self.lo<inst[0] or self.lo>inst[1]:
                                                self.lo=random.gauss(sum(inst)/2,10)
                                        self.lo=round(self.lo)
                                        self.hi=self.lo+12
                                        if self.colorOnly:
                                                self.notes=self.inRange(self.colorTones(self.chordTones(beat)))
                                        else:
                                                self.notes=self.inRange(self.chordTones(beat))
                                else:
                                        self.notes=[]

class Kit(Player):
        def __init__(self,midiBuff,chan,prog,changes,beatsPerMeasure,subDivs,lo=0,hi=midiBuffer.MIDI_MAX):
                Player.__init__(self,midiBuff,chan,prog,changes,beatsPerMeasure,subDivs,lo,hi)
                self.didMiddle=False

                self.hats = [34, 42]
                self.chicks = [69, 70, 82]
                self.rides = [51, 59, 53]
                self.bell = 53
                self.kicks = [35, 36]
                self.sticks = [40, 38, 39] #37
 
                self.balances={self.hats[0]:20, self.hats[1]:20, self.chicks[0]:20, self.chicks[1]:20, self.chicks[2]:20, self.rides[0]:10, self.rides[1]:10, self.rides[2]:10, self.bell:-10, self.kicks[0]:5, self.kicks[1]:5, self.sticks[0]:0, self.sticks[1]:0, self.sticks[2]:0}
                
                
        def chooseNotes(self,beat,subDiv):
                vel=75
                sigma=12
                emph=.22

                if subDiv==2:
                        self.vel=random.gauss(vel*(1+emph),sigma)
                elif subDiv==0 and beat%2==1:
                        self.vel=random.gauss(vel, sigma)                
                else:
                        self.vel=random.gauss(vel*(1-emph), sigma)

                if random.random()>.99 or beat==0:
                        self.chick=random.choice(self.chicks)
                if random.random()>.99 or beat==0:
                        self.ride=random.choice(self.rides)
                if random.random()>.99 or beat==0:
                        self.stick=random.choice(self.sticks)
                if random.random()>.99 or beat==0:
                        self.kick=random.choice(self.kicks)
                if random.random()>.99 or beat==0:
                        self.hat=random.choice(self.hats)       

                self.notes=[]
                if subDiv==0:
                        self.notes.append(self.ride)
                        if beat%2==1:
                                self.notes.append(self.hat)
                                self.notes.append(self.chick)
                elif subDiv==2 and ((beat%2==1 and random.random()>.2) or (beat%2==0 and random.random()>.8)):
                                self.notes.append(self.ride)

                if (subDiv!=1 and random.random()>.8) or (subDiv==2 and self.didMiddle) or (subDiv==1 and random.random()>.95):
                        if random.random()>.4:
                                self.notes.append(self.stick)
                        else:
                                self.notes.append(self.kick)
                        if subDiv==1:
                                self.didMiddle=True
                        else:
                                self.didMiddle=False

                for i in self.notes:
                        self.midiBuffer.playChord([i],.001,self.balances[i]+self.vel,self.chan)
                        
                self.notes=[]
                        
class Application(Frame):
        def tempoC(self,val): pass

        def toggle(self):
                if self.blinker.running:
                        self.blinker.running=False
                        self.startB["text"] = "start"
                        if self.cmdID != []:
                                self.master.after_cancel(self.cmdID)
                                self.cmdID=[]
                        self.labels[self.pos]["bg"]="white"
                else:
                        self.blinker.running=True
                        self.startB["text"] = "stop"
                        self.pos=0
                        self.doBlink()

        def doBlink(self):
                #print ".",
                if self.blinker.state != self.state:
                        self.state=self.blinker.state
                        if self.pos!=self.blinker.newPos:
                                self.labels[self.pos]["bg"]="white"
                                self.pos=self.blinker.newPos
                        if self.state:
                                self.labels[self.pos]["bg"]="red"
                                #self.bell()
                        else:
                                self.labels[self.pos]["bg"]="white"
                self.cmdID=self.master.after(10,self.doBlink)

        def doQuit(self):
                self.blinker.stop=True
                self.blinker.join()
                self.quit()

        def createWidgets(self):
                Cblues=[["C7", "F7", "C7", "C7"], ["F7", "F7", "C7", "C7"], ["G7", "F7", "C7", "C7"]]                                
                CminorBlues=[["C-6", "F7#9", "C-6", ["C-6", "C7"]], ["FmM7", "F13", "C-7", ["C-7", "B7"]], ["G7#11", "Fdim7", ["CmM7", "Db9"], "G7b9"]]                
                CbluesEF=[["C7", "F7", "C13", ["G-9", "C7"]], ["F7", ["E-7b5", "F7"], "C7#9", ["C7#9", "D7#11"]], ["G7", "F7", ["C13","Adim"], ["D-7b5", "G7#9"]]]

                #maiden voyage
                A=["Dsus47","Dsus47","Dsus47","Dsus47","Fsus47","Fsus47","Fsus47","Fsus47"]
                B=["Ebsus47","Ebsus47","Ebsus47","Ebsus47","Dbsus47","Dbsus47","Dbsus47","Dbsus47"]
                maidenVoyage=[A, A, B, A]

                #afro blue
                A=["F-7", "G-7", ["AbM7","G-7"], "F-7"]
                B=["Eb7", "Eb7", ["Db7","Eb7"], "F-7"]
                S=["F-7","F-7","F-7","F-7","F-7","F-7","F-7","F-7"]
                afroBlue=[A,A,B,B,S,A,A,B,B]

                #alone together
                A = ["D-", ["E-7b5","A7b9"], "D-", ["E-7b5","A7b9"], "D-", ["A-7b5","D7b9"], "G-", "G-7"]
                B1 = [["B-7", "E7"],["G-7","C7"],["F","F7"],["E-7b5","A7"],"DM7",["E-7b5","A7"]]
                B2 = [["B-7", "E7"],["G-7","C7"],["F","F7"],["E-7b5","A7"],"DM7","DM7"]

                C = ["A-7b5","D7b9","G-","G-","G-7b5","C7b9",["F","F7"],["E-7b5","A7"]]
                D = ["D-", ["E-7b5","A7b9"], "D-", ["E-7b5","A7b9"], "D-", ["Bb7","A7"],"D-7",["E-7b5","A7"]]
                aloneTogether=[A,B1,A,B2,C,D]

                #i thought about you
                A=[["F6","Bb7"],["A7","D7"],["G7","Ab7"],"G7","G-7",["E-7b5","A7b9"],["D-7","Db7"],["C-7","F7"]]
                B=["BbM7",["Bb-7","Eb7"],["FM7","C7"],"FM7",["B-7b5","E7b9"],["B-7b5","E7b9"],["A-7","D7"],["G-7","C7"]]
                C=["BbM7",["Bb-7","Eb7"],["FM7","D-7"],["B-7b5","Bb7#11"],["A-7","D7"],["G-7","C7"],["F6","D7b9"],["G-7","C7"]]
                thoughtAboutYou=[A,B,A,C]

                self.phrases=aloneTogether
                
                self.beatsPerMeasure=4
                self.swing=True
                self.state=False

                self.quitB = Button(self)
                self.quitB["text"] = "quit"
                self.quitB["command"] = self.doQuit
                self.quitB.grid()

                self.startB = Button(self)
                self.startB["text"] = "start"
                self.startB["command"] = self.toggle
                self.startB.grid()

                self.labels=[]
                self.labelStrs=[]
                self.beats=[]
                f=tkFont.Font(family="Courier",size=50,weight="bold")
                
                self.main=Frame(self)
                self.main.grid()
                self.subFrames=[]
                chords=[]
                for p in range(len(self.phrases)):
                        phrase=self.phrases[p]
                        for m in range(len(phrase)):
                                measure=phrase[m]
                                self.subFrames.append(Frame(self.main))
                                self.subFrames[len(self.subFrames)-1].grid(row=p,column=m,sticky=W)
                                if type(measure)==str:
                                        measure=[measure]
                                        self.phrases[p][m]=[self.phrases[p][m]]
                                for s in range(len(measure)):
                                        self.labelStrs.append(StringVar())
                                        self.labelStrs[len(self.labelStrs)-1].set(self.phrases[p][m][s])
                                        self.labels.append(Label(self.subFrames[len(self.subFrames)-1],textvariable=self.labelStrs[len(self.labelStrs)-1],font=f))
                                        self.labels[len(self.labels)-1].grid(row=0,column=s,padx=10)
                                        beats=float(self.beatsPerMeasure)/len(measure)
                                        if beats>self.beatsPerMeasure/len(measure):
                                                if s%2==0:
                                                        beats=math.ceil(beats)
                                                else:
                                                        beats=math.floor(beats)
                                        self.beats.append(int(beats))
                                        for bt in range(int(beats)):
                                                chords.append(self.phrases[p][m][s])

                self.cumBeats=cumsum(self.beats)
                                                
                self.tempo = Scale(self, command=self.tempoC, from_=30, to=500, label="tempo", orient=HORIZONTAL, length=600)
                self.tempo.set(200)
                self.tempo.grid()

                class Blinker(threading.Thread):
                        def __init__(self,tempo,beats,cumBeats,chords,beatsPerMeasure,subDivs):
                                self.tempo=tempo
                                self.running=False
                                self.beats=beats
                                self.cumBeats=cumBeats
                                self.chords=chords
                                self.beatsPerMeasure=beatsPerMeasure
                                self.subDivs=subDivs
                                threading.Thread.__init__(self)
                                
                        def run(self):
                                b=midiBuffer.midiBuffer(verbose=True,device=[])

                                players=[]
                                gbBass=[36,75]
                                norm=[24,52]
                                logBass=[42,68]
                                inst=gbBass
                                players.append(Bass(b,0,32,self.chords,self.beatsPerMeasure,self.subDivs,inst[0],inst[1])) #acoustic bass chan 1
                                players.append(Comp(b,1,0,self.chords,self.beatsPerMeasure,self.subDivs)) #piano chan 2
                                players.append(Solo(b,2,0,self.chords,self.beatsPerMeasure,self.subDivs,51,85)) #piano chan 3
                                players.append(Kit(b,9,159,self.chords,self.beatsPerMeasure,self.subDivs)) #piano chan 10
                                                                
                                self.stop=False
                                while not self.stop:
                                        if self.running:
                                                x=time.time()
                                                if x>self.baseTime:
                                                        if self.baseTime==0:
                                                                self.baseTime=x
                                                        dur=60.0/self.tempo.get()/self.subDivs
                                                        self.baseTime+=dur

                                                        self.subDiv+=1
                                                        if self.subDiv>=self.subDivs:
                                                                self.subDiv=0
                                                                self.beat+=1
                                                                if self.beat>=sum(self.beats):
                                                                        self.beat=0
                                                                
                                                        self.state=self.subDiv==0
                                                        self.newPos=bisect.bisect(self.cumBeats,self.beat)

                                                        for player in players:
                                                                player.doBeat(self.beat,self.subDiv,1000*dur)

                                                elif self.baseTime-time.time()>.1:
                                                        bed=time.time()
                                                        dur=.9*(self.baseTime-bed)
                                                        time.sleep(dur)
                                                        act=time.time()-bed
                                                        #print "slept for " + str(act) + " (" + str(100*act/dur) + "% accurate)"
                                        else:
                                                self.baseTime=0
                                                self.state=False
                                                self.beat=0
                                                self.subDiv=-1

                                                for player in players:
                                                        player.reset()
                                for player in players:
                                        player.reset()                                                
                                b.close(True)

                if self.swing:
                        subDivs=3
                else:
                        subDivs=4
                self.blinker=Blinker(self.tempo,self.beats,self.cumBeats,chords,self.beatsPerMeasure,subDivs)
                self.blinker.start()

        def __init__(self, master=None):
                Frame.__init__(self, master)
                self.grid()
                self.createWidgets()
                self.master=master             

def main():        
    root = Tk()
    app = Application(master=root)
    app.master.title("pyBass")
    app.master.lift() #why doesn't this work?
    app.mainloop()
    root.destroy()

main()
